package org.breathe.aof.writer.impl;

import lombok.extern.slf4j.Slf4j;
import org.breathe.aof.writer.Writer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author: breathe
 * @createTime: 2025-05-16
 */
@Slf4j
public class AofWriter implements Writer {
    private File file;
    private int flushInterval;
    private FileChannel fileChannel;
    private RandomAccessFile randomAccessFile;
    private boolean preAllocated;
    private AtomicLong size;
    private static final int DEFAULT_PREALLOCATE_SIZE = 4 * 1024 * 1024;
    private final AtomicLong realSize = new AtomicLong(0);
    public AofWriter(File file, boolean preAllocated, int flushInterval, FileChannel fileChannel) throws FileNotFoundException {
        this.file = file;
        this.preAllocated = preAllocated;
        if(fileChannel == null){
            this.randomAccessFile = new RandomAccessFile(file,"rw");
            this.fileChannel = randomAccessFile.getChannel();
            fileChannel = this.fileChannel;
        }else{
            this.fileChannel = fileChannel;
        }
        try {
            this.size = new AtomicLong(fileChannel.size());
            if (this.preAllocated) {
                preAllocate(DEFAULT_PREALLOCATE_SIZE);
            }
            this.fileChannel.position(this.size.get());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 预分配
     *
     * @throws IOException IO异常
     */
    private void preAllocate(int defaultPreallocateSize) throws IOException {
        long currentSize = 0;
        try {
            currentSize = this.fileChannel.size();
        } catch (IOException ex) {
            log.error("error when get file length", ex);
        }
        long newSize = currentSize + defaultPreallocateSize;
        if (this.randomAccessFile != null) {
            this.randomAccessFile.setLength(newSize);
        } else if (this.fileChannel != null) {
            this.fileChannel.truncate(newSize);
        }

        this.fileChannel.position(currentSize);
        this.realSize.set(currentSize);
    }

    @Override
    public int write(ByteBuffer byteBuffer) throws IOException {
        int written = writtenFullyTo(fileChannel, byteBuffer);
        size.addAndGet(written);
        realSize.addAndGet(written);
        return written;
    }

    private int writtenFullyTo(FileChannel fileChannel, ByteBuffer byteBuffer) {
        int originalPosition = byteBuffer.position();
        int originalLimit = byteBuffer.limit();
        int totalBytes = byteBuffer.remaining();
        try {
            int written = 0;
            while (written < totalBytes) {
                written += fileChannel.write(byteBuffer);
            }
            return written;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            byteBuffer.position(originalPosition);
            byteBuffer.limit(originalLimit);
        }
    }

    @Override
    public void flush() throws IOException {
        fileChannel.force(true);
    }

    @Override
    public void close() throws IOException {
        try {
            flush();
            fileChannel.truncate(realSize.get());
            log.info("AOF file has truncate size: {}", realSize.get());
            fileChannel.close();
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
        } catch (IOException ex) {
            log.error("error when close AOF file", ex);
        }
    }
}
